package thread;

import java.util.concurrent.PriorityBlockingQueue;

/**
 * Creared with IntelliJ IDEA.
 * Description:自己实现一个定时器
 * User:yxd
 * Date:2022-03-29
 * Time:15:56
 */
//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask>{
    //任务具体要干啥(标准库中是通过实现runnable接口来描述任务的)
    private Runnable runnable;
    //任务具体啥时候做,保存任务要执行的毫秒级时间戳
    private long time;
    //time 表示的是时间间隔,表示delay时候后再去执行任务
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;//这里加上原本的时间得到的就是开始执行的时间了
    }
    //执行任务
    public void run(){
        runnable.run();
    }
    public long getTime(){
        return time;
    }
    //让myTask类实现一下比较器,来表示按时间先后顺序来排序
    @Override
    public int compareTo(MyTask o) {//this放在前面,按升序排列
        return (int)(this.time - o.time);//比较规则,时间小的在前,时间大的在后
    }
}
//定时器内要存放多个任务,应该用一个数据结构来组织(堆,按照时间先后来执行)
class MyTimer{

    //定时器内部要能够存放多个任务
    //使用普通的堆还不行,因为这里不一定是在单线程环境下了,有可能在多线程环境下,而且未来执行这些线程还需要队列,既要在某些线程里面入队列,又要在某些线程里面出队列,使用普通的堆的话,可能会引起线程安全问题,因此我们要使用的队列是PriorityBlockingQueue
    //这个优先级是既带有阻塞,又带有优先级的优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//需要指定排序标准,手动指定按时间先后顺序排列
    //任务执行
    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);
        queue.put(task);
        //每次插入成功之后我们唤醒一下扫描线程,让线程重新检查一下队首的任务是否时间到了要执行了,重新计算一下等待时间
        synchronized (locker){
            locker.notify();
        }
    }
    private Object locker = new Object();
    //需要先执行时间最靠前的任务,就需要一个线程,去不停的检查当前的优先级队列,看最靠前的任务是否时间已经到了
    public MyTimer(){
        //在构造方法里面设一个线程,让线程不断的扫描队首,看队首时间时候要到了
        Thread t = new Thread(()->{
            while(true){
                try {
                    //如果这里的任务时空的就还好,这个线程就阻塞了,但是队列不为空的话切执行时间还没到,就会一直查看,这样既没有实质性的工作产出,
                    // 又没有进行休息,这被称为"忙等",这是非常耗费资源,非常浪费CPU的,而且没有什么意义,那怎么解决这个问题呢,就可以基于wait这样的机制来实现
                    //wait有一个版本,指定等待时间(不需要notify,时间到了自然唤醒,计算出当前时间和任务的目标之前的时间差,就等待这么长时间就可以了


                    MyTask task = queue.take();//确实直接peek更好一些,但是这里的调整消费并不复杂,堆的调整是logn,还是很小的,这个复杂度并不高,所以使用这两个都是可以的
                    //再比较一下看这个任务的时间是否到了

                    long time = System.currentTimeMillis();//当前时间
                    if(time < task.getTime()){
                        //时间还没到,再将这个任务再塞回去
                        queue.put(task);
                        //此时指定一个等待时间
                        synchronized (locker){
                            locker.wait(task.getTime() - time);//当前需要等待的时间,这里为什么不用sleep,
                            // 因为wait可以提前唤醒,而sleep不能被中途唤醒,
                            //因为在等待过程中,是有可能插入一个比当前任务还要紧急的任务,那么我们就需要提前唤醒等待了
                        }
                    }else{
                        //时间到了
                        task.run();//执行任务

                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
public class Demo24 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000);
        System.out.println("main");
    }
}
